/* custom jQuery plugins */
(function ($) {

    var _ = GlobalResources.constants;

    $.arrayDiff = function (a, b) {
        var diff = [];

        $.each(a, function (i, val) {
            if ($.inArray(val, b) === -1) diff.push(val);
        });

        $.each(b, function (i, val) {
            if ($.inArray(val, a) === -1) diff.push(val);
        });

        return diff;
    };

    $.arrayPartialDiff = function (a, b) {
        var diff = [];

        $.each(a, function (i, val) {
            if ($.inArray(val, b) === -1) diff.push(val);
        });

        return diff;
    };

    $.arrayIntersection = function (a, b) {
        var intersection = [];

        $.each(a, function (i, val) {
            if ($.inArray(val, b) !== -1) intersection.push(val);
        });

        return intersection;
    };

    $.arrayUnique = function (a) {
        var unique = [];

        $.each(a, function (i, val) {
            if ($.inArray(val, unique) === -1) unique.push(val);
        });

        return unique;
    };

    $.arrayDupes = function (a) {
        var unique = [], dupes = [];

        $.each(a, function (i, val) {
            if ($.inArray(val, unique) === -1) {
                unique.push(val);
            } else {
                dupes.push(val);
            }
        });

        return dupes;
    };

    $.getObjectKeys = function (object) {
        var key, keys = [];

        for (key in object) {
            if (object.hasOwnProperty(key)) keys.push(key);
        }

        return keys;
    };

    //Returns an ISO-8601-formatted string representation of the date object.
    $.toISO8601 = function (d) {
        function padzero(n) {
            return n < 10 ? '0' + n : n;
        }
        function pad2zeros(n) {
            if (n < 100) {
                n = '0' + n;
            }
            if (n < 10) {
                n = '0' + n;
            }
            return n;
        }
        return d.getUTCFullYear() + '-' + padzero(d.getUTCMonth() + 1) + '-' + padzero(d.getUTCDate()) + 'T' + padzero(d.getUTCHours()) + ':' + padzero(d.getUTCMinutes()) + ':' + padzero(d.getUTCSeconds()) + '.' + pad2zeros(d.getUTCMilliseconds()) + 'Z';
    };

    // $.extend only clones the outer object but we need to also clone recursively inner objects and arrays.
    $.cloneObject = function (object) {
        var key, value, ret = {};

        for (key in object) {
            if (object.hasOwnProperty(key)) {
                value = object[key];
                if ($.isArray(value)) {
                    ret[key] = value.slice(0);
                } else if (value && (typeof value === 'object')) {
                    ret[key] = $.cloneObject(value);
                } else {
                    ret[key] = value;
                }
            }
        }

        return ret;
    };

    // jQuery.param is broken, {a: null} gets translated to a=null, wat.
    $.toQueryString = function (object) {
        var key, items = [];

        for (key in object) {
            if (object.hasOwnProperty(key) && (!!object[key] || object[key] === 0)) {
                items.push([key + '=' + object[key]]);
            }
        }

        return items.length ? ('?' + items.join('&')) : '';
    };

    $.getFileExtension = (function () {
        var ext_matcher = new RegExp(".([\\w_\\-\\+~#]+)$");

        return function (name) {
            name = (name || '').toLowerCase();
            return ext_matcher.test(name) ? name.match(ext_matcher)[1] : '';
        }
    })();

    $.pluck = function (a, key) {
        return $.map(a, function (b) { return b[key]; });
    };

    $.fn.pluck = function (attr) {
        return $.map(this, function (e) { return e.getAttribute(attr); });
    };

    // time to play with prototypes :)
    if (!"".capitalize) {
        String.prototype.capitalize = function () {
            return this.replace(/\b[a-z]/g, function (match) {
                return match.toLocaleUpperCase();
            });
        };
    }

    // Accepts a string; returns the string with regex metacharacters escaped. The returned string
    // can safely be used at any point within a regex to match the provided literal string. Escaped
    // characters are [ ] { } ( ) * + ? - . , \ ^ $ | # and whitespace
    //Attribution: from XRegExp (http://xregexp.com/) MIT License
    RegExp.escape = function (str) {
        return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
    };

    // requires: [jQuery, jQuery.fancybox(), Mustache, innerShiv, jQuery.fn.spinner(), jQueryUI]
    // provides: jQuery.confirmationPopup()
    $.confirmationPopup = function (options) {
        var default_options = { buttons: [{ label: 'Ok', default_btn: true}],
            loadingText: ''
        },
        template = '<div id="cpcontainer"><div id="confirmationPopup" class="overlay_window">' +
                   '  <header><h1>{{title}}</h1></header>' +
                   '  <div class="body_container"><div class="body">' +
                   '    <span class="message">{{{message}}}</span>' +
                   '  </div></div>' +
                   '  <div class="controls highlighted">' +
                   '    {{#buttons}}<button type="button" data-type="{{label}}">{{label}}</button>{{/buttons}}' +
                   '  </div>' +
                   '</div></div>';
        options = $.extend({}, default_options, options);

        $.fancybox({
            width: 'auto',
            height: 'auto',
            scrolling: 'no',
            padding: 1,
            type: 'inline',
            hideOnOverlayClick: false,
            content: '#confirmationPopup',
            onStart: function () {
                $('body').append(innerShiv(Mustache.to_html(template, options)), false).find('#cpcontainer button').button();
            },
            onComplete: function () {
                $.each(options.buttons, function (i, button) {
                    var btn = $('#confirmationPopup button[data-type="' + button.label + '"]');
                    if (button.default_btn) btn.addClass('default').focus();

                    btn.click(function (e) {
                        e.preventDefault();
                        if (button.callback) {
                            if (button.default_btn && options.loadingText) {
                                $('#confirmationPopup button').not(btn).hide();
                                btn.spinner({ text: options.loadingText });
                            }
                            button.callback(function () {
                                btn.spinner('restore').hide();
                                $.fancybox.close();
                            });
                        } else {
                            $.fancybox.close();
                        }
                    });
                });
            },
            onCleanup: function () {
                $('#cpcontainer').remove();
            }
        });
    };


    $.fn.toggler = function (options) {
        var $target = $(this).hide(),
        state = $target.is(':checked'),
        toggler = $('<div/>', { 'class': 'toggler ' + (state ? 'on' : 'off'), role: 'checkbox', tabindex: 0, 'aria-checked': (state ? 'true' : 'false') }),
        knob = $('<div/>', { 'class': 'greything' }).appendTo(toggler);

        if (state) knob.css({ left: 32 });

        toggler.bind({
            click: function () {
                $target[0].checked = state = !$target[0].checked;
                knob.animate({ left: state ? 32 : 0 }, 100, function () {
                    options.onChange && options.onChange.call(toggler, state);
                });
                toggler.toggleClass('on off').attr({ 'aria-checked': (state ? 'true' : 'false') });
            },
            keydown: function (e) {
                if (e.keyCode === 13 || e.keyCode === 32) $(this).trigger('click');
            }
        });

        $target.replaceWith(toggler).appendTo(toggler);

        return $target;
    };

    $.fn.overlayText = function () {
        return $(this).not('[data-overlay-text-enabled="true"]').each(function (i, field) {
            var $field = $(this),
          overlay_text = this.getAttribute('data-overlay-text'),
          $label = (overlay_text ? $('<label/>', { text: overlay_text }) : $('label[for=' + this.getAttribute('id') + ']')).addClass('overlay_text'),
          coords = $field.position(),
          last_val = $field.val(),
          paddings = {
              top: parseInt($field.css('padding-top'), 10),
              bottom: parseInt($field.css('padding-bottom'), 10),
              left: parseInt($field.css('padding-left'), 10)
          },
          is_empty = function () { return !/[^\s]+/.test($field.val()); },
          fn = {
              hide: function () {
                  $label.hide();
                  if (!is_empty()) $field.trigger('valuechange::content');
              },
              show: function (options) {
                  options = options || {};

                  $field.val('').addClass('empty').trigger('valuechange::empty');
                  if (options.focus) {
                      $field.focus();
                  } else {
                      $label.show();
                  }

                  return $field;
              },
              check: function () {
                  var new_val = $field.val();
                  if (last_val === new_val) return;

                  if (!is_empty()) {
                      $field.removeClass('empty').trigger('valuechange::content');
                  } else {
                      $field.addClass('empty').trigger('valuechange::empty');
                  }
                  last_val = new_val;
              }
          };

            this.setAttribute('data-overlay-text-enabled', 'true');
            coords.left += paddings.left;
            coords.top += paddings.top;

            $label.insertBefore($field).css({
                left: coords.left,
                top: coords.top,
                lineHeight: field.tagName.toLowerCase() === 'textarea' ? null : ($field.outerHeight() - paddings.top - paddings.bottom) + 'px',
                maxWidth: $field.innerWidth(),
                maxHeight: Math.max($field.innerHeight(), parseInt($field.css('line-height'), 10)),
                overflow: 'hidden'
            }).click(function () { $field.focus(); });

            $field.bind({
                'focus keydown': fn.hide,
                'keypress keyup click change': fn.check,
                drop: function () { fn.hide(); fn.check(); },
                blur: function () { if (is_empty()) fn.show(); }
            });
            $field.data({
                reset: fn.show,
                val: function (val) {
                    $field.val(val);
                    if (is_empty()) { $label.show(); $field.addClass('empty'); } else { $label.hide(); $field.removeClass('empty'); }
                }
            });

            fn[is_empty() ? 'show' : 'hide']();
        });
    };

    $.fn.autoGrow = function () {
        return $(this).each(function () {
            var $field = $(this),
          orig_height = $field.height(),
          sizing_bug_compensation,
          resize_fn = function () {
              var prev = $field.height();

              if (sizing_bug_compensation == null) sizing_bug_compensation = (($field.innerHeight() == $field[0].scrollHeight) || ($field.innerHeight() == $field[0].scrollHeight + 1)) ? $field[0].scrollHeight - $field.height() : 0;

              $field.height('auto').height($field[0].scrollHeight - sizing_bug_compensation);

              if ($field.height() !== prev) $field.trigger('autogrow::resize');
          };

            $field.data('orig_height', orig_height)
            .css({ minHeight: orig_height })
            .addClass('autogrow')
            .bind('keydown keypress keyup change blur', resize_fn);
        });
    };

    $.fn.limitLength = function () {
        var fn = {
            commonFilter: function (e) {
                var len = parseInt(this.getAttribute('maxlength'), 10) || 256;

                if (this.value.length > len) {
                    this.value = this.value.substring(0, len);
                    e.preventDefault();
                }
            },
            downFilter: function (e) {
                var len = parseInt(this.getAttribute('maxlength'), 10) || 256;

                if (this.value.length === len && e.which > 32) {
                    e.preventDefault();
                }
            }
        };

        return $(this).bind({ keypress: fn.commonFilter, keyup: fn.commonFilter, blur: fn.commonFilter, keydown: fn.downFilter });
    };

    $.fn.autoselect = function () {
        return $(this).bind({
            focus: function (e) { this.select(); e.preventDefault(); e.stopPropagation(); },
            click: function (e) { this.select(); e.preventDefault(); e.stopPropagation(); },
            mouseup: function (e) { this.select(); e.preventDefault(); e.stopPropagation(); }
        });
    };

    // requires: [jQuery, TaskManager, ScheduledTask]
    // provides: [Tooltip.captureTargets(), Tooltip.forceHide(), Tooltip.reposition() ]
    var Tooltip = this.Tooltip = (function () {
        var tooltip,
        tooltip_content,
        fn = {
            init: function () {
                tooltip = $('#jq-ads-tooltip').length ? $('#jq-ads-tooltip') : $('<div/>', { 'class': 'tooltip', id: 'jq-ads-tooltip' }).html('<span class="pointer"></span><div class="jq-ads-tooltip-content"></div>').appendTo(document.body);
                tooltip_content = tooltip.find('.jq-ads-tooltip-content');
            },

			//calculte the offset between "Tooltip Panel" and "Relative Element ('i' button)"
			toolTipOffset: function(option) {
				var offset_left, offset_top, myPostionSplitedStr, positionOffset;
				myPostionSplitedStr = option.my.split(" ");
				
				//calcute the offset based on the postion of Tooltp panel
				if (myPostionSplitedStr.length === 2)
				{
					if (myPostionSplitedStr[0] === "left")
						offset_left = -20;
					else if (myPostionSplitedStr[0] === "right")
						offset_left = 20;
						
					if (myPostionSplitedStr[1] === "top")
						offset_top = 5;
					else if (myPostionSplitedStr[1] === "bottom")
						offset_top = -5;
						
					if (option.my === "left center")
					{
						offset_left = 10;
						offset_top = 0;
					}
					else if (option.my === "right center")
					{
						offset_left = -10;
						offset_top = 0;
					}
				}
				
				positionOffset = offset_left + " " + offset_top;
				return positionOffset;
			},
			
            reposition: function (option) {
				var positionOffset = fn.toolTipOffset(option);
				tooltip.position({my: option.my, at: option.relative, of: this, offset: positionOffset, collision: 'none'});
           }
        };

        $(document.body).bind('click', function () { Tooltip.forceHide(); });
        $(document).bind('keydown', function (e) { if (e.keyCode === 27) Tooltip.forceHide(); });

        return {
            captureTargets: function (targets, options) {
                // short circuit if all targets are already attached
                targets = targets.not('.has_tooltip');
                if (!targets.length) return;
                
				//Set default values for positions, the tooltip panel is under relative element like"i" button
				//tooltip element position
                options = $.extend({}, { position: 'left top' }, options);
				//relative element position, usually it's "i" button
				options = $.extend({}, { relative_position: 'right bottom' }, options);
							
                targets.each(function () {
                    var $this = $(this),
					$tip = this.getAttribute('data-tooltip-contents') ? $(this.getAttribute('data-tooltip-contents')) : this.getAttribute('data-tooltip'),
					//Get the real postion values from node attributes or use default values
					tooltip_position = this.getAttribute('data-tooltip-position') || options.position,
					relative_position = this.getAttribute('data-relative-position') || options.relative_position;
			  
                    if (($tip === undefined) || ($tip.length === 0) || ((typeof $tip === 'string') && !/[^\s]+/.test($tip))) return;
                    if ($tip instanceof jQuery) {
                        $tip.data('parent', $tip.parent());
                    } else {
                        $tip = null; // nulling so we can dynamically update the tooltip text in the data-tooltip attribute
                    }

                    $this.addClass('has_tooltip').bind({
                        mouseenter: function () {
                            TaskManager.schedule({
                                name: 'tooltip',
                                namespace: 'general',
                                callback_fn: function () {
                                    if (!$this.is(':visible') || $this.hasClass('ui-custom-active')) return;

                                    fn.init();

                                    var content = tooltip_content.children();
                                    if (content.length && content.data('parent')) content.appendTo(content.data('parent'));
                                    tooltip_content.empty();

                                    options.beforeShow && options.beforeShow.call($this, content);

                                    tooltip_content.append($tip || $this.attr('data-tooltip'));
                                    tooltip.stop(true, true).removeClass('left right top bottom center').addClass(tooltip_position).fadeIn(120, function () { options.onShow && options.onShow(); });
                                    tooltip.data('relative', $this);

                                    fn.reposition.call($this, {my: tooltip_position, relative: relative_position});
                                }
                            }, 750);
                        },
                        mouseleave: function () {
							fn.init();

                            TaskManager.remove('tooltip');
                            tooltip.stop(true, true).fadeOut(120, function () { options.onHide && options.onHide.call($this, tooltip_content.children()); });
						}
                    });
                });
            },
            forceHide: function (killTimer) {
                fn.init();
                if (killTimer) TaskManager.remove('tooltip');
                tooltip.stop(true, true).fadeOut(0);
            },
            reposition: function () {
                if (tooltip && tooltip.is(':visible')) {
				    var options;
					
					//Set default values for positions, the tooltip panel is under relative element like"i" button
					//tooltip element position
					options = $.extend({}, { position: 'left top' }, options);
					//relative element position, usually it's "i" button
					options = $.extend({}, { relative_position: 'right bottom' }, options);
					
					//Get the real postion values from node attributes or use default values
					tooltip_position = this.getAttribute('data-tooltip-position') || options.position,
					relative_position = this.getAttribute('data-relative-position') || options.relative_position;
					
                    fn.reposition.call(tooltip.data('relative'), {my: tooltip_position, relative: relative_position});
                }
            }
        };
    })();


    // requires: jQuery
    // provides: PopupMessage.create()
    var PopupMessage = this.PopupMessage = {
        create: function (contents, options) {
            var default_options = {
                position: 'left',
                closeButton: true
            };

            options = $.extend({}, default_options, options);

            // short circuit if targets don't exist
            var targets = options.targets || $(contents.attr('data-targets'));
            if (!targets.length) return;

            var type = options.type || contents.attr('data-type'),
          title = options.title || contents.attr('data-title'),
          message_position = options.position || contents.attr('data-message-position'),
          message = $('<div/>', { 'class': 'jq-ads-popup-message' + (title ? ' with_title' : '') + (type ? (' ' + type) : '') }).html('<span class="pointer"></span><div class="jq-ads-message-content"><div class="body"></div></div>'),
          message_content = message.find('.jq-ads-message-content');

            var closeMessage = function () {
                message.stop(true, true).fadeOut(100, function () { options.onHide && options.onHide(); });
            };

            if (title) {
                message_content.prepend($('<header>' + title + '</header>'));
                message.addClass('with_title');
            }

            if (options.closeButton) {
                message_content.append($('<span class="close"></span>'));
                message_content.find('.close').bind('click', closeMessage);
            }

            targets.each(function () {
                var $this = $(this),
            $tip = contents,
            reposition_this = function () {
                message.position({ my: message_position + ' top', at: message_position + ' bottom', of: $this, collision: 'none', offset: '0 12' });
            };

                if (($tip === undefined) || ($tip.length === 0) || ((typeof $tip === 'string') && !/[^\s]+/.test($tip))) return;
                if (typeof $tip !== 'string') $tip.data('parent', $tip.parent());

                var events = {};
                events['popup::open'] = function (e, popupId) {
                    if (popupId && contents.attr('id') != popupId) return;

                    options.beforeShow && options.beforeShow();

                    message.appendTo(options.attach_global ? document.body : $this.offsetParent());

                    message_content.find('.body').append($tip);
                    message.stop(true, true).removeClass('left right').addClass(message_position).fadeIn(100, function () { options.onShow && options.onShow($tip, $this); });
                    reposition_this();
                    $(window).unbind('resize.popup', reposition_this).bind('resize.popup', reposition_this);
                    e.stopPropagation();
                };
                if (options.eventOpen) events[options.eventOpen] = events['popup::open'];
                events['popup::close'] = function (e) {
                    $(window).unbind('resize.popup', reposition_this);
                    closeMessage();
                    e.stopPropagation();
                };
                if (options.eventClose) events[options.eventClose] = events['popup::close'];

                $this.bind(events);

                if (((contents.attr('data-autoshow') || '').toLowerCase() === 'true') || options.autoshow) $this.trigger('popup::open', [contents.attr('id')]);
            });
        }
    };

    var TimeUtils = this.TimeUtils = (function () {

        var use12Hour = true,
        fn = {
            toReadableTime: function (t) {
                var pm = use12Hour && (t.getHours() > 11),
                hours = use12Hour ? (t.getHours() % 12 || 12) : t.getHours(),
                minutes = t.getMinutes() + "";

                return hours + ":" + (minutes.length === 1 ? "0" : "") + minutes + (use12Hour ? (" " + (pm ? _["post_meridiem"] : _["ante_meridiem"])) : "");
            }
        };

        return {
            set12HourClock: function (use) {
                use12Hour = use;
            },

            relativize: function ($target) {
                var currentTime = new Date(),
            currentTimeUTC = currentTime.getTime(),
            currentTimeFullYear = currentTime.getFullYear(),
            startOfDay = new Date(currentTimeFullYear, currentTime.getMonth(), currentTime.getDate()),
            startOfDayUTC = startOfDay.getTime(),
            yesterdayUTC = startOfDayUTC - 86400000,
            months = _['months_short'].split(',');

                $.each($target, function (i, time) {
                    var dateTime = new Date(parseInt(time.getAttribute('datetime'), 10)),
              exact = time.getAttribute('data-exact'),
              dateTimeUTC = dateTime.getTime(),
              timeDiff = currentTimeUTC - dateTimeUTC,
              dateTimeFullYear = dateTime.getFullYear(),
              sameYear = dateTimeFullYear === currentTimeFullYear,
              friendlyTime = "";

                    if (exact || dateTimeUTC < yesterdayUTC) {
                        friendlyTime = Mustache.to_html(sameYear ? _['datetime_format'] : _['datetime_format_with_year'], { time: fn.toReadableTime(dateTime), month: months[dateTime.getMonth()], day: dateTime.getDate(), year: dateTimeFullYear });
                    } else {
                        if (timeDiff <= 120000) { //2 minutes ago
                            friendlyTime = _["moments_ago"];
                        } else if (timeDiff <= 3000000) { //50 minutes ago
                            friendlyTime = Mustache.to_html(_['minutes_ago'], { minutes: Math.ceil(timeDiff / 60000.0) });
                        } else if (timeDiff <= 5400000) { //90 minutes ago
                            friendlyTime = _['about_an_hour_ago'];
                        } else if (dateTimeUTC >= startOfDayUTC) { //same day
                            friendlyTime = Mustache.to_html(_['hours_ago'], { hours: Math.ceil(timeDiff / 3600000.0) });
                        } else if (dateTimeUTC >= yesterdayUTC) { //yesterday
                            friendlyTime = Mustache.to_html(_['yesterday_at'], { time: fn.toReadableTime(dateTime) });
                        }
                    }

                    while (time.firstChild) time.removeChild(time.firstChild);
                    time.appendChild(document.createTextNode(friendlyTime));
                });
            }
        }
    })();

    // requires: jQuery
    // provides: FlashMessage
    var FlashMessage = this.FlashMessage = function (options) {
        this.options = options || {};

        var self = this,
        $element, timer,
        fn = {
            reposition: function () {
                $element.css({ top: -$element.outerHeight(), marginLeft: -$element.outerWidth() / 2 });
            },
            init: function () {
                $element = $('#flash_message').length ? $('#flash_message') : $('<div/>', { id: 'flash_message' }).appendTo(document.body);

                if ($element.data('flash')) $element.data('flash').detach();
                $element.data('flash', self);

                $element.html(self.options.text).shorten();
                if (self.options.type) $element.addClass(self.options.type);
                if (self.options.icon && self.options.icon.parent() != $element) self.options.icon.prependTo($element);

                fn.reposition();
            },
            detach: function () {
                fn.clearTimer();
                if (self.options.icon) self.options.icon.detach();
                if (self.options.type) $element.removeClass(self.options.type);
                $element.empty();
                $element.removeData('flash');
            },
            clearTimer: function () {
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
            }
        };

        this.toElement = function () { return $element; };

        this.show = function () {
            fn.init();
            if (this.options.onShow) this.options.onShow();

            $element.stop(true).animate({ top: -3 }, 1);
            if (this.options.autohide) {
                fn.clearTimer();
                timer = setTimeout(this.hide, options.hideTime || 6000);
            }
        };

        this.hide = function () {
            if (self.options.onHide) self.options.onHide();
            fn.clearTimer();
            $element.stop(true).animate({ top: -$element.outerHeight() }, 133, fn.detach);
        };

        this.detach = fn.detach;
    };

    // requires: [jQuery, FlashMessage]
    // provides: [GlobalSpinner.show(), GlobalSpinner.hide()]
    var GlobalSpinner = this.GlobalSpinner = (function () {
        var spinner, flash, busy,
        fn = {
            init: function () {
                spinner = new Spinner({ yoffset: -16 });
                flash = new FlashMessage({
                    text: _['spinner_loading'],
                    icon: spinner.toElement(),
                    type: 'global_spinner',
                    onShow: spinner.start,
                    onHide: spinner.stop
                });
            }
        };

        return {
            show: function (text) {
                if (!flash) fn.init();

                if (typeof text === "string" && text.length)
                    flash.options.text = text;
                else
                    flash.options.text = _['spinner_loading'];

                if (flash.toElement() && flash.toElement().data('flash')) return; // low priority for the GlobalSpinner

                busy = true;
                flash.show();
            },

            hide: function () {
                if (flash && flash.toElement().hasClass('global_spinner') && busy) flash.hide();
                busy = false;
            }
        };
    })();

    // requires: jQuery
    // provides: Fling
    var Fling = this.Fling = function (options) {
        var $element,
        handle = null,
        position = 0,
        step_size = 1,
        default_options = {
            piece_size: 40,
            pixel_speed: 150,
            interval: 20,
            yoffset: 0,
            autorun: false,
            onComplete: function () { }
        };

        options = options || {};
        for (var key in default_options) {
            if (default_options.hasOwnProperty(key)) {
                options[key] = options[key] || default_options[key];
            }
        }

        step_size = options.pixel_speed / (1000 / options.interval);
        $element = options.element;
        position = $element.width() + options.piece_size;

        $element.css({
            backgroundPosition: position + ' ' + options.yoffset + 'px',
            width: options.size,
            height: options.size
        }).addClass(options.spinner_class);

        var next = function () {
            position -= step_size;
            if (position <= -options.piece_size) {
                this.stop();
                options.onComplete.call(this, options);
            }

            $element.css({ backgroundPosition: position + 'px ' + options.yoffset + 'px' });
        };

        this.start = function (size) {
            if (!handle) {
                position = (size || $element.width()) + options.piece_size;
                handle = setInterval($.proxy(next, this), options.interval);
            }
            return handle;
        };

        this.stop = function () {
            if (handle) {
                clearInterval(handle);
                handle = null;
            }
        };

        this.toElement = function () { return $element; };
        this.toString = function () { return handle; };

        if (options.autorun) this.start();
    };

    // requires: jQuery
    // provides: Spinner
    var Spinner = this.Spinner = function (options) {
        var $spinner = null,
        handle = null,
        position = 0,
        default_options = {
            frames: 12,
            size: 16,
            yoffset: 0,
            interval: 60,
            autorun: false,
            parent: document.body,
            element: null,
            spinner_class: 'spinner'
        };

        options = options || {};
        for (var key in default_options) {
            if (default_options.hasOwnProperty(key)) {
                options[key] = options[key] || default_options[key];
            }
        }

        $spinner = options.element ? $(options.element) : $('<div/>').appendTo(options.parent);
        $spinner.css({
            backgroundPosition: '0 ' + options.yoffset + 'px',
            width: options.size,
            height: options.size
        }).addClass(options.spinner_class);

        var next = function () {
            position = (position + 1) % options.frames;

            $spinner.css({ backgroundPosition: '-' + (position * options.size) + 'px ' + options.yoffset + 'px' });
        };

        this.start = function () {
            if (!handle) handle = setInterval(next, options.interval);
            return handle;
        };

        this.stop = function () {
            if (handle) {
                clearInterval(handle);
                handle = null;
            }
        };

        this.toElement = function () { return $spinner; };
        this.toString = function () { return handle; };

        if (options.autorun) this.start();
    };

    // requires: [jQuery, Spinner]
    // provides: jQuery.fn.spinner()
    $.fn.spinner = function (options) {
        options = options || {};
        var $target = $(this), $element, spinner, spinner_options;

        if (typeof options === 'string') {
            $.each($target, function (i, element) {
                $element = $(element);
                spinner = $element.data('spinner');

                if (spinner && options === 'restore') {
                    spinner_options = $element.data('spinner_options');

                    spinner.stop();
                    if (spinner_options.text) {
                        $element.removeClass('spinned');
                        spinner.toElement().parent().remove();
                    } else {
                        $element.removeClass('with_spinner');
                        spinner.toElement().remove();
                    }

                    $element.removeData('spinner_options');
                    $element.removeData('spinner');
                }
            });
        } else {
            $.each($target, function (i, element) {
                $element = $(element);
                spinner = new Spinner(options);

                options.text = options.text || $element.attr('data-loading-text');

                $element.data({
                    spinner_options: options,
                    spinner: spinner
                });

                if (options.text) {
                    spinner.toElement().prependTo($('<div/>', { 'class': 'spinner_container', text: options.text, click: false }).insertBefore($element));
                    $element.addClass('spinned');
                } else {
                    spinner.toElement().prependTo($element);
                    $element.addClass('with_spinner');
                }
                spinner.start();
            });
        }

        return this;
    };

    var ClosableElements = this.ClosableElements = (function () {
        var hooked_elements = [];

        $(document).bind('keydown', function (e) { if (e.keyCode === 27) ClosableElements.close(); });

        return {
            add: function (selectors, shared_callback, semi_modal) {
                selectors = $.makeArray(selectors);

                if ((hooked_elements.length === 0) && selectors.length) {
                    // attach global event when we add the first set of elements
                    $(document).click(ClosableElements.close);
                }

                if (selectors.length) {
                    $.each(selectors, function (index, elem) {
                        if ($.inArray(elem, hooked_elements) === -1) hooked_elements.push(elem);
                        if (shared_callback) $(elem).bind('closable::close', shared_callback);
                        if (semi_modal) {
                            $(elem).bind({
                                click: function (e) { if (e) e.stopPropagation(); },
                                contextmenu: function (e) { if (e) e.preventDefault(); }
                            });
                        }
                    });
                }
            },

            close: function () {
                return $(hooked_elements.join(',')).filter('.open').trigger('closable::close').removeClass('open');
            }
        };
    })();

    // requires: jQuery
    // provides: TaskManager
    var TaskManager = this.TaskManager = (function () {
        var tasks = [];

        return {
            getTask: function (name) {
                for (var i = 0, j = tasks.length; i < j; i++) {
                    if (tasks[i].name === name) return $.extend({}, tasks[i], { index: i });
                }
            },

            taskExists: function (name) {
                return !!TaskManager.getTask(name);
            },

            register: function (options) {
                if (this.taskExists(options.name)) TaskManager.remove(options.name);

                tasks.push(options);
            },

            run: function (name, data) {
                var task = TaskManager.getTask(name);

                if (task) {
                    return task.callback_fn.call(task, task.data || data);
                }
            },

            remove: function (name) {
                var task = TaskManager.getTask(name);

                if (task) {
                    tasks.splice(task.index, 1);
                    if (task.cleanup_fn) return task.cleanup_fn.call(task, task.data);
                }
            },

            killAll: function (namespace) {
                var i, task;

                if (namespace) {
                    for (i = tasks.length - 1; i >= 0; i--) {
                        if (tasks[i].namespace === namespace) {
                            task = tasks.splice(i, 1)[0];
                            if (task.cleanup_fn) task.cleanup_fn.call(task, task.data);
                        }
                    }
                } else {
                    for (i = tasks.length - 1; i >= 0; i--) {
                        task = tasks[i];
                        if (task.cleanup_fn) task.cleanup_fn(task, task.data);
                    }
                    tasks = [];
                }
            }
        };
    })();

    // requires: [jQuery, TaskManager]
    // provides: [TaskManager.schedule(), TaskManager.reschedule(), TaskManager.pause(), TaskManager.taskIsScheduled()]
    var ScheduledTask = function (options, period) {
        this.period = period || 15000; // arbitrary default delay of 15 seconds.
        this.options = options;

        var prev_cleanup = options.cleanup_fn || function () { };
        this.options.cleanup_fn = function (data) {
            this.instance.stop();
            prev_cleanup(data);
        };

        options.instance = this;

        if (TaskManager.taskExists(this.options.name)) TaskManager.remove(this.options.name);
        this.schedule();
    };

    $.extend(ScheduledTask.prototype, {
        schedule: function (period) {
            if (period) this.period = period;

            this.stop();
            this.timer_id = setTimeout($.proxy(this.run, this), this.period);
            if (!TaskManager.taskExists(this.options.name)) TaskManager.register(this.options);
        },

        stop: function () {
            if (this.timer_id) {
                clearTimeout(this.timer_id);
                this.timer_id = null;
            }
        },

        run: function () {
            this.stop();
            TaskManager.run(this.options.name);
            if (this.options.auto_reschedule) this.schedule();
        },

        destroy: function () {
            this.stop();
            TaskManager.remove(this.options.name);
        }
    });

    $.extend(TaskManager, {
        schedule: function (options, period) {
            return new ScheduledTask(options, period);
        },

        reschedule: function (name, period) {
            if (TaskManager.taskExists(name)) {
                var task = TaskManager.getTask(name);
                if (task.instance instanceof ScheduledTask) task.instance.schedule(period);
            }
        },

        taskIsScheduled: function (name) {
            if (TaskManager.taskExists(name)) {
                var task = TaskManager.getTask(name);
                return (task.instance instanceof ScheduledTask) && !task.timer_id;
            }
        },

        pause: function (name) {
            if (TaskManager.taskExists(name)) {
                var task = TaskManager.getTask(name);
                if (task.instance instanceof ScheduledTask) task.instance.stop();
            }
        }
    });

    // requires: [jQuery]
    // provides: [ContactList.create(list), ContactList.getSelectedContacts()]
    // options: target, appendTo, contactsChangedCallback
    var ContactList = this.ContactList = function (list, options) {
        var $list = $(list),
        $parent = $list.parent(),
        $target = $(options.target),
        state = {},
        diff_state = {}, //once opened, new checkbox changes are registered here
        diff_number = 0, //number of changes registered
        that = this;


        //create state
        var createState = function () {
            $.each(
        $.map($list.find('li'), function (contact) {
            var $contact = $(contact);
            return { email: $contact.attr('data-email'),
                firstName: $contact.attr('data-first-name'),
                lastName: $contact.attr('data-last-name'),
                selected: $contact.hasClass('checked')
            };
        }), function (i, contact) {
            state[contact.email] = contact;
        }
      );
        };

        createState();

        ClosableElements.add(list, function () {
            that.hide();
        }, true);

        //Sets a specific existing contact as selected. Contact must exist in list. FIXME
        var setContactSelected = function (email, selected) {
            if ($list.hasClass('open')) {
                if (diff_state[email] != undefined && diff_state[email] != selected) { //checkbox is back to original state
                    diff_number--;
                    delete diff_state[email];
                } else {
                    diff_state[email] = selected;
                    diff_number++;
                }
                if (diff_number) {
                    $list.find('.add').removeAttr('disabled');
                } else {
                    $list.find('.add').attr('disabled', 'disabled');
                }
            } else {
                state[email].selected = selected;
            }
        };

        $list.delegate('.custom_checkbox', 'click', function () {
            var $this = $(this);
            $this.toggleClass('checked');
            setContactSelected($this.attr('data-email'), $this.hasClass('checked'));
        });

        $list.find('.add').click(function () {
            $list.removeClass('open').appendTo($parent);
            var list = {};
            for (var contact in diff_state) {
                if (diff_state.hasOwnProperty(contact)) {
                    state[contact].selected = diff_state[contact];
                    list[contact] = state[contact];
                }
            }
            if (options.contactsChangedCallback) {
                options.contactsChangedCallback(list);
            }

            $list.data('relative').removeClass('ui-custom-active');

            diff_state = {};
            diff_number = 0;
        });

        $(options.target).click(function (e) {
            that.show(e);
        });

        this.show = function (e) {
            if (!$list.hasClass('open')) {

                $list.data('relative', $target);
                Nitrous.util.toggleElement(e, {
                    element: $list,
                    hide_fn: function (options) {
                        that.hide();
                    }
                });

                $list.find('.add').attr('disabled', 'disabled');

                Nitrous.util.reposition_floater($list, $target, { position: 'left' }); // FIXME: violating sandboxed scope, need to move reposition_floater to plugins file
                Nitrous.util.reposition_floater($list, $target, { position: 'left' }); // redundancy brought to you by jQuery: write more, do less.
            }
        };

        this.hide = function () {
            if ($list.hasClass('open')) {
                $list
          .removeClass('open')
          .appendTo($parent);

                var contacts = $list.find('li');
                for (var contactKey in diff_state) {
                    if (diff_state.hasOwnProperty(contactKey)) {
                        contacts.filter('[data-email="' + state[contactKey].email + '"]').toggleClass('checked');
                    }
                }

                diff_state = {};
                diff_number = 0;
                $list.data('relative').removeClass('ui-custom-active');
            }
        };

        this.setContactSelected = function (email, selected) {
            var $contact = $list.find('li[data-email="' + email + '"]');
            if ($contact.length && $contact.hasClass('checked') != selected) {
                $contact.toggleClass('checked');
                setContactSelected(email, selected);
            }
        };

        this.getContactsMap = function () {
            var map = {};
            for (var contactKey in state) {
                if (state.hasOwnProperty(contactKey)) {
                    var contact = state[contactKey];
                    map[contact.lastName ? (contact.firstName + ' ' + contact.lastName + ' <' + contact.email + '>') : contact.email] = contact;
                }
            }
            return map;
        };

        this.getContacts = function () {
            return state;
        };

        this.addContact = function (firstName, lastName, email, selected) {
            //need to sort alphabetically by last name, what a pain...
            //can't add if it's open
            if (!$list.hasClass('open')) {

                if ($list.find('li[data-email="' + email + '"]').length) return; //short circuit if contact already exists.

                if (!$list.find('ol').length) {
                    $list.find('div.empty').replaceWith('<ol></ol>');
                }

                //add to state
                if (state[email] == undefined) {
                    state[email] = { firstName: firstName, lastName: lastName, email: email, selected: selected };
                }

                var contacts = [];
                for (var contactKey in state) {
                    if (state.hasOwnProperty(contactKey)) {
                        var contact = state[contactKey];
                        contacts.push(contact);
                    }
                }
                contacts = contacts.sort(function (a, b) {
                    if ((a.lastName ? a.lastName.toLowerCase() : a.email) < (b.lastName ? b.lastName.toLowerCase() : b.email)) return -1;
                    if ((a.lastName ? a.lastName.toLowerCase() : a.email) > (b.lastName ? b.lastName.toLowerCase() : b.email)) return 1;
                    return 0;
                });

                var i = 0;
                while (contacts[i].email != email) {
                    i++;
                }
                var previous = i == 0 ? null : contacts[i - 1];
                var li = '<li class="custom_checkbox ' + (selected ? 'checked' : '') + '" data-email="' + email + '" data-invited="false" data-active="false" ' + (lastName ? ('data-first-name="' + firstName + '" data-last-name="' + lastName + '"') : '') + '>' + (lastName ? (firstName + ' ' + lastName) : email) + '</li>';
                if (previous != null) {
                    $list.find('li[data-email="' + previous.email + '"]').after(li);
                } else {
                    $list.find('ol').prepend(li);
                }
            } else {
                throw "Cannot add contacts to open list, yet";
            }
        };

        this.deleteContact = function (email) {
            if ($list.hasClass('open')) throw "";
            var li = $list.find('li[data-email="' + email + '"]');
            if (!li.length) return;
            li.remove();
            delete state[email];

            if (!$list.find('li').length) {
                $list.find('ol').replaceWith('<div class="empty">' + _['contact_list_empty_message'] + '</div>');
            }
        };

        this.isEmpty = function () {
            return !$list.find('ol').length;
        };

        this.exists = function (email) {
            return $list.find('li[data-email="' + email + '"]').length;
        };

    };

    var ContactGrid = this.ContactGrid = function (options) {
        var self = this,
        $table,
        default_options = {
            onStatusChange: function () { },
            onAdd: function () { }
        };

        options = $.extend({}, default_options, options);

        var fn = {
            init: function () {
                $table = $(options.element).addClass('contact_grid');

                if (options.access_menu_selector) {
                    fn.initAccessLevelMenu(options.access_menu_selector);
                }

                $table.find('tbody')
          .delegate('span.remove', 'click', function () { self.remove($(this).closest('tr').attr('data-uid')); })
          .delegate('span.undo', 'click', function () { self.restore($(this).closest('tr').attr('data-uid')); })
          .delegate('tr.contact_status_invited .custom_checkbox', 'click', function () {
              $(this).closest('tr').toggleClass('extra_invite');
              fn.statusChange();
          });
            },

            insertEmptyRow: function () {
                var table = $table[0],
            tr = table.getElementsByTagName('tbody')[0].insertRow(0);

                tr.insertCell(-1); tr.insertCell(-1); tr.insertCell(-1); tr.insertCell(-1);
                tr = $(tr).addClass('odd');
                tr.find('td').html('&nbsp;').eq(0).attr({ 'class': 'contact_identity' }).end()
                                    .eq(1).attr({ 'class': 'contact_access' }).html('<span>&nbsp;</span>').end()
                                    .eq(2).attr({ 'class': 'contact_status' });

                $table.find('tbody tr').toggleClass('odd even');

                return tr;
            },

            initAccessLevelMenu: function (menu_selector) {
                var $menu = $(menu_selector),
            help_panel_open = true;

                Nitrous.callbacks.registerFloater({
                    toggle_fn: function (e, floater_options) {
                        var $this = $(this),
                $element = $(floater_options.element);

                        if (!Nitrous.util.toggleElement(e, floater_options)) return;

                        $element.find('ul.options').find('li.selected').removeClass('selected').end()
                                       .find('li.hover').removeClass('hover').end()
                                       .find('li[data-value="' + $this.attr('data-level') + '"]').addClass('selected');

                        var $panel = $element.find('section.help_panel');
                        $element.find('ul.options li').each(function () {
                            var $this = $(this),
                  $pointer = $panel.find('[data-for="' + $this.attr('data-value') + '"] span.pointer');

                            $pointer.css({ top: $this.offset().top - $element.offset().top + ($this.height() - $pointer.height()) / 2 });
                        });

                        // reset help panel
                        $element.find('.left_panel .help_trigger').removeClass('checked');
                        $element.width('auto');
                        $panel.removeClass('open');

                        // select default panel based on current value
                        showHelpPanelFor($element.find('ul.options li.selected'));

                        Nitrous.util.reposition_floater($element, $this, { position: floater_options.position, event: e });

                        // show again help panel if needed, this gets the panel to look as an 'add-on' for the menu
                        if (help_panel_open) $element.find('.left_panel .help_trigger').click();

                        $element.trigger('closable::open');
                    },
                    hide_fn: Nitrous.util.hideElement,
                    position: 'right',
                    listener: options.element,
                    toggler: 'tbody tr:not(.contact_status_removed,.contact_status_readonly) td.contact_access',
                    idOwner: 'tr',
                    templateContainer: options.element + ' + ' + '.templates',
                    element: menu_selector,
                    semi_modal: true
                });

                var showHelpPanelFor = function (element) {
                    var $panel = $menu.find('section.help_panel');

                    if (!$panel.length) return;

                    $panel.find('.visible').removeClass('visible');
                    $panel[element.hasClass('selected') ? 'addClass' : 'removeClass']('selected');
                    $panel.find('[data-for="' + element.attr('data-value') + '"]').addClass('visible');
                };

                $menu.find('.options').delegate('li', 'click', function () {
                    var target = $menu.data('relative'),
              new_level = this.getAttribute('data-value');

                    if (target.attr('data-level') !== new_level) {
                        if (!target.data('prev-level')) target.data('prev-level', target.attr('data-level'));

                        target.attr('data-level', new_level)
                  .find('span').html(this.getAttribute('data-level-shortname'));
                    }
                    if (!target.closest('tr').hasClass('contact_status_new')) target[(new_level !== target.data('prev-level')) ? 'addClass' : 'removeClass']('modified');

                    target.trigger('dropdown::change');
                    $(document.body).trigger('click');
                }).delegate('li', {
                    mouseenter: function () {
                        var $this = $(this);

                        $this.addClass('hover');
                        showHelpPanelFor($this);
                    },
                    mouseleave: function () {
                        var $this = $(this);

                        $this.removeClass('hover');
                        showHelpPanelFor($this.parent().find('.selected'));
                    }
                });

                $menu.find('.left_panel .help_trigger').bind('click', function () {
                    help_panel_open = $(this).toggleClass('checked').hasClass('checked');
                    $menu.find('.help_panel').toggleClass('open');
                    $menu.width($menu.find('.left_panel').outerWidth() + $menu.find('.help_panel').filter(':visible').outerWidth() - 1);
                });

                $table.delegate('li:not(.selected)', 'mouseleave', function () {
                    showHelpPanelFor($menu.find('ul.options li.selected'));
                }).bind('dropdown::change', function () {
                    fn.statusChange();
                });
            },

            statusChange: function () {
                var status = self.getStatus(),
            status_bar = $table.find('tfoot td'),
            status_obj = {
                removed_sharees: status.removed,
                removed_sharees_one: status.removed === 1,
                new_sharees: status.new_invites,
                new_sharees_one: status.new_invites === 1,
                total_sharees: status.total,
                total_sharees_one: status.total === 1,
                contact_name_singular: options.contact_name.singular,
                contact_name_plural: options.contact_name.plural
            };

                $table[status.trs.length ? 'removeClass' : 'addClass']('empty');

                status_bar.html(Mustache.to_html(_['contact_statustext_removed'] + _['contact_statustext_new'] + _['contact_statustext_total'], status_obj));

                options.onStatusChange && options.onStatusChange(status);
            }
        };

        fn.init();


        this.add = function (contact) {
            var tr = fn.insertEmptyRow().addClass('contact_status_new').attr({ 'data-uid': contact.email.toLowerCase(), 'data-contact-status': 'new' }),
          tds = tr.find('td'),
          thumb = '<img src="' + $table.attr('data-user-thumb-placeholder') + '" alt="" /> ';

            tds.eq(0).html(thumb + '<span>' + (contact.lastName ? (contact.firstName + ' ' + contact.lastName) : contact.email.toLowerCase()) + '</span>').end()
         .eq(1).attr('data-level', $table.attr('data-default-access-level')).find('span').html($table.attr('data-default-access-shortname')).end().end()
         .eq(2).html(_['contact_status_new']).end()
         .eq(3).html('<span class="custom_checkbox"></span><span class="row_icon remove"></span>');

            if (options.contact_list && options.contact_list.exists(contact.email)) tr.data('contact', contact);

            fn.statusChange();
            options.onAdd && options.onAdd();
        };

        this.remove = function (uid) {
            var tr = this.getElementsByContactId(uid),
          position = tr.index();

            if (tr.hasClass('contact_status_new')) {
                var contact = tr.data('contact');
                if (contact && options.contact_list) options.contact_list.addContact(contact.firstName, contact.lastName, contact.email, false);
                $table.find('tbody')[0].deleteRow(position);
                $table.find('tbody tr').slice(position).toggleClass('odd even');
            } else {
                tr.addClass('contact_status_removed');
                tr.find('td').eq(0).find('img').css({ opacity: 0.5 }).end().end()
                     .eq(1).data('prev-text', tr.find('td:eq(1) span').html()).find('span').html(_['contact_access_noaccess']).end().end()
                     .eq(2).html(_['contact_status_removed']).end()
                     .eq(3).append($('<span/>').addClass('undo').html(_['contact_grid_undo']));
            }

            fn.statusChange();
        };

        this.restore = function (uid) {
            var tr = this.getElementsByContactId(uid);

            tr.removeClass('contact_status_removed');
            tr.find('td').eq(0).find('img').css({ opacity: 1 }).end().end()
                   .eq(1).find('span').html(tr.find('td:eq(1)').data('prev-text')).end().end()
                   .eq(2).html(tr.hasClass('contact_status_active') ? _['contact_status_active'] : _['contact_status_invited']).end()
                   .eq(3).find('span.undo').remove();

            fn.statusChange();
        };

        this.has = function (uid) {
            return !!this.getElementsByContactId(uid).length;
        };

        this.getDeleted = function () {
            return $table.find('tr.contact_status_removed').pluck('data-uid');
        };

        this.getNew = function () {
            return $table.find('tr.contact_status_new').pluck('data-uid');
        };

        this.getModifiedAccess = function () {
            return $table.find('tr:has(td.contact_access.modified)').not('.contact_status_removed').pluck('data-uid');
        };

        this.getContactsToInvite = function () {
            return $table.find('tr.extra_invite, tr.contact_status_new').not('.contact_status_removed').pluck('data-uid');
        };

        this.getDiff = function () {
            return this.getDeleted().concat($.arrayUnique(this.getContactsToInvite().concat(this.getModifiedAccess())));
        };

        this.getElementsByContactId = function (ids) {
            var DOMquery = $.map($.makeArray(ids), function (id) { return 'tr[data-uid="' + id + '"]'; }).join(',');
            return $table.find(DOMquery);
        };

        this.refresh = function () {
            fn.statusChange();
        };

        this.getStatus = function () {
            var trs = $table.find('tbody tr'),
          total_invites = this.getContactsToInvite().length,
          removed = this.getDeleted().length;

            return {
                trs: trs,
                diff: this.getDiff(),
                total_invites: total_invites,
                new_invites: this.getNew().length,
                removed: removed,
                total: trs.length - removed,
                has_invites: !!total_invites
            };
        };

        this.getContactsData = function () {
            var diff = this.getDiff(),
          $elements = this.getElementsByContactId(diff),
          data = { UserChanges: [] };

            $elements.each(function () {
                var $this = $(this),
            id = $this.attr('data-uid'),
            action, user;

                if ($this.is('tr.contact_status_removed')) {
                    action = 'delete';
                    user = $this.is('tr.contact_status_active') ? { Id: id} : { Email: id };
                } else if ($this.is('tr.extra_invite, tr.contact_status_new')) {
                    action = 'invite';
                    user = { Email: id };
                    if ($this.is('tr.contact_status_new') || $this.find('td.contact_access.modified').length) user.AccessLevel = $this.find('td.contact_access').attr('data-level');
                } else if ($this.find('td.contact_access.modified').length) {
                    action = 'modify';
                    user = $this.is('tr.contact_status_active') ? { Id: id} : { Email: id };
                    user.AccessLevel = $this.find('td.contact_access.modified').attr('data-level');
                }

                data.UserChanges.push({ Action: action, User: user });
            });

            return data;
        };
    };

})(jQuery);
